// ***************************************************************** -*- C++ -*-
/*
 * Copyright (C) 2004-2019 Exiv2 authors
 * This program is part of the Exiv2 distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
 */

#include "params.hpp"

#include "actions.hpp"
#include "i18n.h"  // NLS support.
#include <exiv2/convert.hpp>
#include <exiv2/error.hpp>

#if defined(_MSC_VER)
#include <Windows.h>
#endif

#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW__) || defined(_MSC_VER)
#include <windows.h>
#include <fcntl.h>
#include <io.h>
#else
// select & fd_set for musl libc
#include <sys/select.h>
#endif

#include <fstream>
#include <string>

namespace
{
    // Evaluate [-]HH[:MM[:SS]], returns true and sets time to the value
    // in seconds if successful, else returns false.
    bool parseTime(const std::string& ts, long& time);

    /*!
      @brief Parse one line of the command file
      @param modifyCmd Reference to a command structure to store the parsed command
      @param line Input line
      @param num Line number (used for error output)
     */
    bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num);

    /*!
      @brief Parse metadata modification commands from multiple files
      @param modifyCmds Reference to a structure to store the parsed commands
      @param cmdFiles Container with the file names
     */
    bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles);

    /*!
      @brief Parse metadata modification commands from a container of commands
      @param modifyCmds Reference to a structure to store the parsed commands
      @param cmdLines Container with the commands
     */
    bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines);

    /*!
      @brief Parse the oparg string into a bitmap of common targets.
      @param optarg Option arguments
      @param action Action being processed
      @return A bitmap of common targets or -1 in case of a parse error
     */
    int parseCommonTargets(const std::string& optarg, const std::string& action);

    /*!
      @brief Parse numbers separated by commas into container
      @param previewNumbers Container for the numbers
      @param optarg Option arguments
      @param j Starting index into optarg
      @return Number of characters processed
     */
    int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optarg, int j);

    /*!
      @brief Parses a string containing backslash-escapes
      @param input Input string, assumed to be UTF-8
     */
    std::string parseEscapes(const std::string& input);

    int readFileToBuf(FILE* f, Exiv2::DataBuf& buf);

    /// Return a command Id for a command string
    CmdId commandId(const std::string& cmdString);

    //! List of all command identifiers and corresponding strings
    const CmdIdAndString cmdIdAndString[] = {
        {add, "add"}, {set, "set"}, {del, "del"}, {reg, "reg"}, {invalidCmdId, "invalidCmd"}  // End of list marker
    };
}  // namespace

Params::Params()
    : optstring_(":hVvqfbuktTFa:Y:O:D:r:p:P:d:e:i:c:m:M:l:S:g:K:n:Q:"),
      first_(true),
      help_(false),
      version_(false),
      verbose_(false),
      force_(false),
      binary_(true),
      unknown_(true),
      preserve_(false),
      timestamp_(false),
      timestampOnly_(false),
      fileExistsPolicy_(askPolicy),
      adjust_(false),
      printMode_(pmSummary),
      printItems_(0),
      printTags_(Exiv2::mdNone),
      action_(0),
      target_(ctExif | ctIptc | ctComment | ctXmp),
      adjustment_(0),
      format_("%Y%m%d_%H%M%S"),
      formatSet_(false)
{
    yodAdjust_[yodYear] = {false, "-Y", 0};
    yodAdjust_[yodMonth] = {false, "-O", 0};
    yodAdjust_[yodDay] = {false, "-D", 0};
}

Params& Params::instance()
{
    static Params ins;
    return ins;
}

Params::~Params()
{
}

void Params::version(bool verbose, std::ostream& os) const
{
    os << EXV_PACKAGE_STRING << std::endl;
    if (Params::instance().greps_.empty()) {
        os << "\n"
           << _("This program is free software; you can redistribute it and/or\n"
                "modify it under the terms of the GNU General Public License\n"
                "as published by the Free Software Foundation; either version 2\n"
                "of the License, or (at your option) any later version.\n")
           << "\n"
           << _("This program is distributed in the hope that it will be useful,\n"
                "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
                "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
                "GNU General Public License for more details.\n")
           << "\n"
           << _("You should have received a copy of the GNU General Public\n"
                "License along with this program; if not, write to the Free\n"
                "Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n"
                "Boston, MA 02110-1301 USA\n");
    }

    if (verbose)
        Exiv2::dumpLibraryInfo(os, Params::instance().greps_);
}

void Params::usage(std::ostream& os) const
{
    os << _("Usage:") << " " << progname() << " " << _("[ options ] [ action ] file ...\n\n")
       << _("Manipulate the Exif metadata of images.\n");
}

void Params::help(std::ostream& os) const
{
    usage(os);
    // clang-format off
    os << _("\nActions:\n")
       << _("  ad | adjust   Adjust Exif timestamps by the given time. This action\n"
            "                requires at least one of the -a, -Y, -O or -D options.\n")
       << _("  pr | print    Print image metadata.\n")
       << _("  rm | delete   Delete image metadata from the files.\n")
       << _("  in | insert   Insert metadata from corresponding *.exv files.\n"
            "                Use option -S to change the suffix of the input files.\n")
       << _("  ex | extract  Extract metadata to *.exv, *.xmp and thumbnail image files.\n")
       << _("  mv | rename   Rename files and/or set file timestamps according to the\n"
            "                Exif create timestamp. The filename format can be set with\n"
            "                -r format, timestamp options are controlled with -t and -T.\n")
       << _("  mo | modify   Apply commands to modify (add, set, delete) the Exif and\n"
            "                IPTC metadata of image files or set the JPEG comment.\n"
            "                Requires option -c, -m or -M.\n")
       << _("  fi | fixiso   Copy ISO setting from the Nikon Makernote to the regular\n"
            "                Exif tag.\n")
       << _("  fc | fixcom   Convert the UNICODE Exif user comment to UCS-2. Its current\n"
            "                character encoding can be specified with the -n option.\n")
       << _("\nOptions:\n")
       << _("   -h      Display this help and exit.\n")
       << _("   -V      Show the program version and exit.\n")
       << _("   -v      Be verbose during the program run.\n")
       << _("   -q      Silence warnings and error messages during the program run (quiet).\n")
       << _("   -Q lvl  Set log-level to d(ebug), i(nfo), w(arning), e(rror) or m(ute).\n")
       << _("   -b      Show large binary values.\n")
       << _("   -u      Show unknown tags.\n")
       << _("   -g key  Only output info for this key (grep).\n")
       << _("   -K key  Only output info for this key (exact match).\n")
       << _("   -n enc  Charset to use to decode UNICODE Exif user comments.\n")
       << _("   -k      Preserve file timestamps (keep).\n")
       << _("   -t      Also set the file timestamp in 'rename' action (overrides -k).\n")
       << _("   -T      Only set the file timestamp in 'rename' action, do not rename\n"
            "           the file (overrides -k).\n")
       << _("   -f      Do not prompt before overwriting existing files (force).\n")
       << _("   -F      Do not prompt before renaming files (Force).\n")
       << _("   -a time Time adjustment in the format [-]HH[:MM[:SS]]. This option\n"
            "           is only used with the 'adjust' action.\n")
       << _("   -Y yrs  Year adjustment with the 'adjust' action.\n")
       << _("   -O mon  Month adjustment with the 'adjust' action.\n")
       << _("   -D day  Day adjustment with the 'adjust' action.\n")
       << _("   -p mode Print mode for the 'print' action. Possible modes are:\n")
       << _("             s : print a summary of the Exif metadata (the default)\n")
       << _("             a : print Exif, IPTC and XMP metadata (shortcut for -Pkyct)\n")
       << _("             e : print Exif metadata (shortcut for -PEkycv)\n")
       << _("             t : interpreted (translated) Exif data (-PEkyct)\n")
       << _("             v : plain Exif data values (-PExgnycv)\n")
       << _("             h : hexdump of the Exif data (-PExgnycsh)\n")
       << _("             i : IPTC data values (-PIkyct)\n")
       << _("             x : XMP properties (-PXkyct)\n")
       << _("             c : JPEG comment\n")
       << _("             p : list available previews\n")
       << _("             C : print ICC profile embedded in image\n")
       << _("             R : recursive print structure of image\n")
       << _("             S : print structure of image\n")
       << _("             X : extract XMP from image\n")
       << _("   -P flgs Print flags for fine control of tag lists ('print' action):\n")
       << _("             E : include Exif tags in the list\n")
       << _("             I : IPTC datasets\n")
       << _("             X : XMP properties\n")
       << _("             x : print a column with the tag number\n")
       << _("             g : group name\n")
       << _("             k : key\n")
       << _("             l : tag label\n")
       << _("             n : tag name\n")
       << _("             y : type\n")
       << _("             c : number of components (count)\n")
       << _("             s : size in bytes\n")
       << _("             v : plain data value\n")
       << _("             t : interpreted (translated) data\n")
       << _("             h : hexdump of the data\n")
       << _("   -d tgt  Delete target(s) for the 'delete' action. Possible targets are:\n")
       << _("             a : all supported metadata (the default)\n")
       << _("             e : Exif section\n")
       << _("             t : Exif thumbnail only\n")
       << _("             i : IPTC data\n")
       << _("             x : XMP packet\n")
       << _("             c : JPEG comment\n")
       << _("   -i tgt  Insert target(s) for the 'insert' action. Possible targets are\n"
            "           the same as those for the -d option, plus a modifier:\n"
            "             X : Insert metadata from an XMP sidecar file <file>.xmp\n"
            "           Only JPEG thumbnails can be inserted, they need to be named\n"
            "           <file>-thumb.jpg\n")
       << _("   -e tgt  Extract target(s) for the 'extract' action. Possible targets\n"
            "           are the same as those for the -d option, plus a target to extract\n"
            "           preview images and a modifier to generate an XMP sidecar file:\n"
            "             p[<n>[,<m> ...]] : Extract preview images.\n"
            "             X : Extract metadata to an XMP sidecar file <file>.xmp\n")
       << _("   -r fmt  Filename format for the 'rename' action. The format string\n"
            "           follows strftime(3). The following keywords are supported:\n")
       << _("             :basename:   - original filename without extension\n")
       << _("             :dirname:    - name of the directory holding the original file\n")
       << _("             :parentname: - name of parent directory\n")
       << _("           Default filename format is ")
       <<               format_ << ".\n"
       << _("   -c txt  JPEG comment string to set in the image.\n")
       << _("   -m file Command file for the modify action. The format for commands is\n"
            "           set|add|del <key> [[<type>] <value>].\n")
       << _("   -M cmd  Command line for the modify action. The format for the\n"
            "           commands is the same as that of the lines of a command file.\n")
       << _("   -l dir  Location (directory) for files to be inserted from or extracted to.\n")
       << _("   -S .suf Use suffix .suf for source files for insert command.\n\n");
    // clang-format on
}  // Params::help

int Params::option(int opt, const std::string& optarg, int optopt)
{
    int rc = 0;
    switch (opt) {
        case 'h':
            help_ = true;
            break;
        case 'V':
            version_ = true;
            break;
        case 'v':
            verbose_ = true;
            break;
        case 'q':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute);
            break;
        case 'Q':
            rc = setLogLevel(optarg);
            break;
        case 'k':
            preserve_ = true;
            break;
        case 'b':
            binary_ = false;
            break;
        case 'u':
            unknown_ = false;
            break;
        case 'f':
            force_ = true;
            fileExistsPolicy_ = overwritePolicy;
            break;
        case 'F':
            force_ = true;
            fileExistsPolicy_ = renamePolicy;
            break;
        case 'g':
            rc = evalGrep(optarg);
            break;
        case 'K':
            rc = evalKey(optarg);
            printMode_ = pmList;
            break;
        case 'n':
            charset_ = optarg;
            break;
        case 'r':
            rc = evalRename(opt, optarg);
            break;
        case 't':
            rc = evalRename(opt, optarg);
            break;
        case 'T':
            rc = evalRename(opt, optarg);
            break;
        case 'a':
            rc = evalAdjust(optarg);
            break;
        case 'Y':
            rc = evalYodAdjust(yodYear, optarg);
            break;
        case 'O':
            rc = evalYodAdjust(yodMonth, optarg);
            break;
        case 'D':
            rc = evalYodAdjust(yodDay, optarg);
            break;
        case 'p':
            rc = evalPrint(optarg);
            break;
        case 'P':
            rc = evalPrintFlags(optarg);
            break;
        case 'd':
            rc = evalDelete(optarg);
            break;
        case 'e':
            rc = evalExtract(optarg);
            break;
        case 'C':
            rc = evalExtract(optarg);
            break;
        case 'i':
            rc = evalInsert(optarg);
            break;
        case 'c':
            rc = evalModify(opt, optarg);
            break;
        case 'm':
            rc = evalModify(opt, optarg);
            break;
        case 'M':
            rc = evalModify(opt, optarg);
            break;
        case 'l':
            directory_ = optarg;
            break;
        case 'S':
            suffix_ = optarg;
            break;
        case ':':
            std::cerr << progname() << ": " << _("Option") << " -" << static_cast<char>(optopt) << " "
                      << _("requires an argument\n");
            rc = 1;
            break;
        case '?':
            std::cerr << progname() << ": " << _("Unrecognized option") << " -" << static_cast<char>(optopt) << "\n";
            rc = 1;
            break;
        default:
            std::cerr << progname() << ": " << _("getopt returned unexpected character code") << " " << std::hex << opt
                      << "\n";
            rc = 1;
            break;
    }
    return rc;
}  // Params::option

int Params::setLogLevel(const std::string& optarg)
{
    int rc = 0;
    const char logLevel = tolower(optarg[0]);
    switch (logLevel) {
        case 'd':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::debug);
            break;
        case 'i':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::info);
            break;
        case 'w':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::warn);
            break;
        case 'e':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::error);
            break;
        case 'm':
            Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute);
            break;
        default:
            std::cerr << progname() << ": " << _("Option") << " -Q: " << _("Invalid argument") << " \"" << optarg
                      << "\"\n";
            rc = 1;
            break;
    }
    return rc;
}  // Params::setLogLevel

// http://stackoverflow.com/questions/874134/find-if-string-ends-with-another-string-in-c
static inline bool ends_with(std::string const& value, std::string const& ending, std::string& stub)
{
    if (ending.size() > value.size())
        return false;
    bool bResult = std::equal(ending.rbegin(), ending.rend(), value.rbegin());
    stub = bResult ? value.substr(0, value.length() - ending.length()) : value;
    return bResult;
}

int Params::evalGrep(const std::string& optarg)
{
    int result = 0;
    std::string pattern;
    std::string ignoreCase("/i");
    const bool bIgnoreCase = ends_with(optarg, ignoreCase, pattern);
    const auto flags =
        bIgnoreCase ? (re::regex_constants::ECMAScript | re::regex_constants::icase) : re::regex_constants::ECMAScript;

    greps_.push_back(re::regex(pattern, flags));

    return result;
}

int Params::evalKey(const std::string& optarg)
{
    int result = 0;
    keys_.push_back(optarg);
    return result;
}  // Params::evalKey

int Params::evalRename(int opt, const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
            action_ = Action::rename;
            switch (opt) {
                case 'r':
                    format_ = optarg;
                    formatSet_ = true;
                    break;
                case 't':
                    timestamp_ = true;
                    break;
                case 'T':
                    timestampOnly_ = true;
                    break;
            }
            break;
        case Action::rename:
            if (opt == 'r' && (formatSet_ || timestampOnly_)) {
                std::cerr << progname() << ": " << _("Ignoring surplus option") << " -r \"" << optarg << "\"\n";
            } else {
                format_ = optarg;
                formatSet_ = true;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option") << " -" << (char)opt << " "
                      << _("is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalRename

int Params::evalAdjust(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
        case Action::adjust:
            if (adjust_) {
                std::cerr << progname() << ": " << _("Ignoring surplus option -a") << " " << optarg << "\n";
                break;
            }
            action_ = Action::adjust;
            adjust_ = parseTime(optarg, adjustment_);
            if (!adjust_) {
                std::cerr << progname() << ": " << _("Error parsing -a option argument") << " `" << optarg << "'\n";
                rc = 1;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option -a is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalAdjust

int Params::evalYodAdjust(const Yod& yod, const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:  // fall-through
        case Action::adjust:
            if (yodAdjust_[yod].flag_) {
                std::cerr << progname() << ": " << _("Ignoring surplus option") << " " << yodAdjust_[yod].option_ << " "
                          << optarg << "\n";
                break;
            }
            action_ = Action::adjust;
            yodAdjust_[yod].flag_ = true;
            if (!Util::strtol(optarg.c_str(), yodAdjust_[yod].adjustment_)) {
                std::cerr << progname() << ": " << _("Error parsing") << " " << yodAdjust_[yod].option_ << " "
                          << _("option argument") << " `" << optarg << "'\n";
                rc = 1;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option") << " " << yodAdjust_[yod].option_ << " "
                      << _("is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalYodAdjust

int Params::evalPrint(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
            switch (optarg[0]) {
                case 's':
                    action_ = Action::print;
                    printMode_ = pmSummary;
                    break;
                case 'a':
                    rc = evalPrintFlags("kyct");
                    break;
                case 'e':
                    rc = evalPrintFlags("Ekycv");
                    break;
                case 't':
                    rc = evalPrintFlags("Ekyct");
                    break;
                case 'v':
                    rc = evalPrintFlags("Exgnycv");
                    break;
                case 'h':
                    rc = evalPrintFlags("Exgnycsh");
                    break;
                case 'i':
                    rc = evalPrintFlags("Ikyct");
                    break;
                case 'x':
                    rc = evalPrintFlags("Xkyct");
                    break;
                case 'c':
                    action_ = Action::print;
                    printMode_ = pmComment;
                    break;
                case 'p':
                    action_ = Action::print;
                    printMode_ = pmPreview;
                    break;
                case 'C':
                    action_ = Action::print;
                    printMode_ = pmIccProfile;
                    break;
                case 'R':
#ifdef NDEBUG
                    std::cerr << progname() << ": " << _("Action not available in Release mode") << ": '" << optarg
                              << "'\n";
                    rc = 1;
#else
                    action_ = Action::print;
                    printMode_ = pmRecursive;
#endif
                    break;
                case 'S':
                    action_ = Action::print;
                    printMode_ = pmStructure;
                    break;
                case 'X':
                    action_ = Action::print;
                    printMode_ = pmXMP;
                    break;
                default:
                    std::cerr << progname() << ": " << _("Unrecognized print mode") << " `" << optarg << "'\n";
                    rc = 1;
                    break;
            }
            break;
        case Action::print:
            std::cerr << progname() << ": " << _("Ignoring surplus option -p") << optarg << "\n";
            break;
        default:
            std::cerr << progname() << ": " << _("Option -p is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalPrint

int Params::evalPrintFlags(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
            action_ = Action::print;
            printMode_ = pmList;
            for (std::size_t i = 0; i < optarg.length(); ++i) {
                switch (optarg[i]) {
                    case 'E':
                        printTags_ |= Exiv2::mdExif;
                        break;
                    case 'I':
                        printTags_ |= Exiv2::mdIptc;
                        break;
                    case 'X':
                        printTags_ |= Exiv2::mdXmp;
                        break;
                    case 'x':
                        printItems_ |= prTag;
                        break;
                    case 'g':
                        printItems_ |= prGroup;
                        break;
                    case 'k':
                        printItems_ |= prKey;
                        break;
                    case 'l':
                        printItems_ |= prLabel;
                        break;
                    case 'n':
                        printItems_ |= prName;
                        break;
                    case 'y':
                        printItems_ |= prType;
                        break;
                    case 'c':
                        printItems_ |= prCount;
                        break;
                    case 's':
                        printItems_ |= prSize;
                        break;
                    case 'v':
                        printItems_ |= prValue;
                        break;
                    case 't':
                        printItems_ |= prTrans;
                        break;
                    case 'h':
                        printItems_ |= prHex;
                        break;
                    case 'V':
                        printItems_ |= prSet | prValue;
                        break;
                    default:
                        std::cerr << progname() << ": " << _("Unrecognized print item") << " `" << optarg[i] << "'\n";
                        rc = 1;
                        break;
                }
            }
            break;
        case Action::print:
            std::cerr << progname() << ": " << _("Ignoring surplus option -P") << optarg << "\n";
            break;
        default:
            std::cerr << progname() << ": " << _("Option -P is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalPrintFlags

int Params::evalDelete(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
            action_ = Action::erase;
            target_ = 0;
            // fallthrough
        case Action::erase:
            rc = parseCommonTargets(optarg, "erase");
            if (rc > 0) {
                target_ |= rc;
                rc = 0;
            } else {
                rc = 1;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option -d is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalDelete

int Params::evalExtract(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
        case Action::modify:
            action_ = Action::extract;
            target_ = 0;
            // fallthrough
        case Action::extract:
            rc = parseCommonTargets(optarg, "extract");
            if (rc > 0) {
                target_ |= rc;
                rc = 0;
            } else {
                rc = 1;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option -e is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalExtract

int Params::evalInsert(const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
        case Action::modify:
            action_ = Action::insert;
            target_ = 0;
            // fallthrough
        case Action::insert:
            rc = parseCommonTargets(optarg, "insert");
            if (rc > 0) {
                target_ |= rc;
                rc = 0;
            } else {
                rc = 1;
            }
            break;
        default:
            std::cerr << progname() << ": " << _("Option -i is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalInsert

int Params::evalModify(int opt, const std::string& optarg)
{
    int rc = 0;
    switch (action_) {
        case Action::none:
            action_ = Action::modify;
            // fallthrough
        case Action::modify:
        case Action::extract:
        case Action::insert:
            if (opt == 'c')
                jpegComment_ = parseEscapes(optarg);
            if (opt == 'm')
                cmdFiles_.push_back(optarg);  // parse the files later
            if (opt == 'M')
                cmdLines_.push_back(optarg);  // parse the commands later
            break;
        default:
            std::cerr << progname() << ": " << _("Option") << " -" << (char)opt << " "
                      << _("is not compatible with a previous option\n");
            rc = 1;
            break;
    }
    return rc;
}  // Params::evalModify

int Params::nonoption(const std::string& argv)
{
    int rc = 0;
    bool action = false;
    if (first_) {
        // The first non-option argument must be the action
        first_ = false;
        if (argv == "ad" || argv == "adjust") {
            if (action_ != Action::none && action_ != Action::adjust) {
                std::cerr << progname() << ": " << _("Action adjust is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::adjust;
        }
        if (argv == "pr" || argv == "print") {
            if (action_ != Action::none && action_ != Action::print) {
                std::cerr << progname() << ": " << _("Action print is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::print;
        }
        if (argv == "rm" || argv == "delete") {
            if (action_ != Action::none && action_ != Action::erase) {
                std::cerr << progname() << ": " << _("Action delete is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::erase;
        }
        if (argv == "ex" || argv == "extract") {
            if (action_ != Action::none && action_ != Action::extract && action_ != Action::modify) {
                std::cerr << progname() << ": " << _("Action extract is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::extract;
        }
        if (argv == "in" || argv == "insert") {
            if (action_ != Action::none && action_ != Action::insert && action_ != Action::modify) {
                std::cerr << progname() << ": " << _("Action insert is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::insert;
        }
        if (argv == "mv" || argv == "rename") {
            if (action_ != Action::none && action_ != Action::rename) {
                std::cerr << progname() << ": " << _("Action rename is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::rename;
        }
        if (argv == "mo" || argv == "modify") {
            if (action_ != Action::none && action_ != Action::modify) {
                std::cerr << progname() << ": " << _("Action modify is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::modify;
        }
        if (argv == "fi" || argv == "fixiso") {
            if (action_ != Action::none && action_ != Action::fixiso) {
                std::cerr << progname() << ": " << _("Action fixiso is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::fixiso;
        }
        if (argv == "fc" || argv == "fixcom" || argv == "fixcomment") {
            if (action_ != Action::none && action_ != Action::fixcom) {
                std::cerr << progname() << ": " << _("Action fixcom is not compatible with the given options\n");
                rc = 1;
            }
            action = true;
            action_ = Action::fixcom;
        }
        if (action_ == Action::none) {
            // if everything else fails, assume print as the default action
            action_ = Action::print;
        }
    }
    if (!action) {
        files_.push_back(argv);
    }
    return rc;
}  // Params::nonoption

void Params::getStdin(Exiv2::DataBuf& buf)
{
    // copy stdin to stdinBuf
    if (stdinBuf.size_ == 0) {
#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW__) || defined(_MSC_VER)
        DWORD fdwMode;
        _setmode(fileno(stdin), O_BINARY);
        Sleep(300);
        if (!GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &fdwMode)) {  // failed: stdin has bytes!
#else
        // http://stackoverflow.com/questions/34479795/make-c-not-wait-for-user-input/34479916#34479916
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);
        struct timeval timeout = {1, 0};  // yes: set timeout seconds,microseconds

        // if we have something in the pipe, read it
        if (select(1, &readfds, nullptr, nullptr, &timeout)) {
#endif
#ifdef EXIV2_DEBUG_MESSAGES
            std::cerr << "stdin has data" << std::endl;
#endif
            readFileToBuf(stdin, stdinBuf);
        }
#ifdef EXIV2_DEBUG_MESSAGES
        // this is only used to simulate reading from stdin when debugging
        // to simulate exiv2 -pX foo.jpg                | exiv2 -iXX- bar.jpg
        //             exiv2 -pX foo.jpg > ~/temp/stdin ; exiv2 -iXX- bar.jpg
        if (stdinBuf.size_ == 0) {
            const char* path = "/Users/rmills/temp/stdin";
            FILE* f = fopen(path, "rb");
            if (f) {
                readFileToBuf(f, stdinBuf);
                fclose(f);
                std::cerr << "read stdin from " << path << std::endl;
            }
        }
#endif
#ifdef EXIV2_DEBUG_MESSAGES
        std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size_ << std::endl;
#endif
    }

    // copy stdinBuf to buf
    if (stdinBuf.size_) {
        buf.alloc(stdinBuf.size_);
        memcpy(buf.pData_, stdinBuf.pData_, buf.size_);
    }
#ifdef EXIV2_DEBUG_MESSAGES
    std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size_ << std::endl;
#endif

}  // Params::getStdin()

int Params::getopt(int argc, char* const Argv[])
{
    char** argv = new char*[argc + 1];
    argv[argc] = nullptr;
    std::map<std::string, std::string> longs;

    longs["--adjust"] = "-a";
    longs["--binary"] = "-b";
    longs["--comment"] = "-c";
    longs["--delete"] = "-d";
    longs["--days"] = "-D";
    longs["--force"] = "-f";
    longs["--Force"] = "-F";
    longs["--grep"] = "-g";
    longs["--help"] = "-h";
    longs["--insert"] = "-i";
    longs["--keep"] = "-k";
    longs["--key"] = "-K";
    longs["--location"] = "-l";
    longs["--modify"] = "-m";
    longs["--Modify"] = "-M";
    longs["--encode"] = "-n";
    longs["--months"] = "-O";
    longs["--print"] = "-p";
    longs["--Print"] = "-P";
    longs["--quiet"] = "-q";
    longs["--log"] = "-Q";
    longs["--rename"] = "-r";
    longs["--suffix"] = "-S";
    longs["--timestamp"] = "-t";
    longs["--Timestamp"] = "-T";
    longs["--unknown"] = "-u";
    longs["--verbose"] = "-v";
    longs["--Version"] = "-V";
    longs["--version"] = "-V";
    longs["--years"] = "-Y";

    for (int i = 0; i < argc; i++) {
        std::string* arg = new std::string(Argv[i]);
        if (longs.find(*arg) != longs.end()) {
            argv[i] = ::strdup(longs[*arg].c_str());
        } else {
            argv[i] = ::strdup(Argv[i]);
        }
        delete arg;
    }

    int rc = Util::Getopt::getopt(argc, argv, optstring_);
    // Further consistency checks
    if (help_ || version_) {
        goto cleanup;
    }
    if (action_ == Action::none) {
        // This shouldn't happen since print is taken as default action
        std::cerr << progname() << ": " << _("An action must be specified\n");
        rc = 1;
    }
    if (action_ == Action::adjust && !adjust_ && !yodAdjust_[yodYear].flag_ && !yodAdjust_[yodMonth].flag_ &&
        !yodAdjust_[yodDay].flag_) {
        std::cerr << progname() << ": " << _("Adjust action requires at least one -a, -Y, -O or -D option\n");
        rc = 1;
    }
    if (action_ == Action::modify && cmdFiles_.empty() && cmdLines_.empty() && jpegComment_.empty()) {
        std::cerr << progname() << ": " << _("Modify action requires at least one -c, -m or -M option\n");
        rc = 1;
    }
    if (0 == files_.size()) {
        std::cerr << progname() << ": " << _("At least one file is required\n");
        rc = 1;
    }
    if (rc == 0 && !cmdFiles_.empty()) {
        // Parse command files
        if (!parseCmdFiles(modifyCmds_, cmdFiles_)) {
            std::cerr << progname() << ": " << _("Error parsing -m option arguments\n");
            rc = 1;
        }
    }
    if (rc == 0 && !cmdLines_.empty()) {
        // Parse command lines
        if (!parseCmdLines(modifyCmds_, cmdLines_)) {
            std::cerr << progname() << ": " << _("Error parsing -M option arguments\n");
            rc = 1;
        }
    }
    if (rc == 0 && (!cmdFiles_.empty() || !cmdLines_.empty())) {
        // We'll set them again, after reading the file
        Exiv2::XmpProperties::unregisterNs();
    }
    if (!directory_.empty() && !(action_ == Action::insert || action_ == Action::extract)) {
        std::cerr << progname() << ": " << _("-l option can only be used with extract or insert actions\n");
        rc = 1;
    }
    if (!suffix_.empty() && !(action_ == Action::insert)) {
        std::cerr << progname() << ": " << _("-S option can only be used with insert action\n");
        rc = 1;
    }
    if (timestamp_ && !(action_ == Action::rename)) {
        std::cerr << progname() << ": " << _("-t option can only be used with rename action\n");
        rc = 1;
    }
    if (timestampOnly_ && !(action_ == Action::rename)) {
        std::cerr << progname() << ": " << _("-T option can only be used with rename action\n");
        rc = 1;
    }

cleanup:
    // cleanup the argument vector
    for (int i = 0; i < argc; i++)
        ::free((void*)argv[i]);
    delete[] argv;

    return rc;
}  // Params::getopt

namespace
{
    void printUnrecognizedArgument(const char argc, const std::string& action)
    {
        std::cerr << Params::instance().progname() << ": " << _("Unrecognized ") << action << " " << _("target") << " `"
                  << argc << "'\n";
    }

    bool parseTime(const std::string& ts, long& time)
    {
        std::string hstr, mstr, sstr;
        char* cts = new char[ts.length() + 1];
        strcpy(cts, ts.c_str());
        char* tmp = ::strtok(cts, ":");
        if (tmp)
            hstr = tmp;
        tmp = ::strtok(0, ":");
        if (tmp)
            mstr = tmp;
        tmp = ::strtok(0, ":");
        if (tmp)
            sstr = tmp;
        delete[] cts;

        int sign = 1;
        long hh(0), mm(0), ss(0);
        // [-]HH part
        if (!Util::strtol(hstr.c_str(), hh))
            return false;
        if (hh < 0) {
            sign = -1;
            hh *= -1;
        }
        // check for the -0 special case
        if (hh == 0 && hstr.find('-') != std::string::npos)
            sign = -1;
        // MM part, if there is one
        if (mstr != "") {
            if (!Util::strtol(mstr.c_str(), mm))
                return false;
            if (mm > 59)
                return false;
            if (mm < 0)
                return false;
        }
        // SS part, if there is one
        if (sstr != "") {
            if (!Util::strtol(sstr.c_str(), ss))
                return false;
            if (ss > 59)
                return false;
            if (ss < 0)
                return false;
        }

        time = sign * (hh * 3600 + mm * 60 + ss);
        return true;
    }  // parseTime

#if defined(_MSC_VER) || defined(__MINGW__)
    static std::string formatArg(const char* arg)
    {
        std::string result = "";
        char b = ' ';
        char e = '\\';
        std::string E = std::string("\\");
        char q = '\'';
        std::string Q = std::string("'");
        bool qt = false;
        char* a = (char*)arg;
        while (*a) {
            if (*a == b || *a == e || *a == q)
                qt = true;
            if (*a == q)
                result += E;
            if (*a == e)
                result += E;
            result += std::string(a, 1);
            a++;
        }
        if (qt)
            result = Q + result + Q;

        return result;
    }
#endif

    bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num)
    {
        const std::string delim = " \t";

        // Skip empty lines and comments
        std::string::size_type cmdStart = line.find_first_not_of(delim);
        if (cmdStart == std::string::npos || line[cmdStart] == '#')
            return false;

        // Get command and key
        std::string::size_type cmdEnd = line.find_first_of(delim, cmdStart + 1);
        std::string::size_type keyStart = line.find_first_not_of(delim, cmdEnd + 1);
        std::string::size_type keyEnd = line.find_first_of(delim, keyStart + 1);
        if (cmdStart == std::string::npos || cmdEnd == std::string::npos || keyStart == std::string::npos) {
            std::string cmdLine;
#if defined(_MSC_VER) || defined(__MINGW__)
            for (int i = 1; i < __argc; i++) {
                cmdLine += std::string(" ") + formatArg(__argv[i]);
            }
#endif
            throw Exiv2::Error(Exiv2::kerErrorMessage,
                               Exiv2::toString(num) + ": " + _("Invalid command line:") + cmdLine);
        }

        std::string cmd(line.substr(cmdStart, cmdEnd - cmdStart));
        CmdId cmdId = commandId(cmd);
        if (cmdId == invalidCmdId) {
            throw Exiv2::Error(Exiv2::kerErrorMessage,
                               Exiv2::toString(num) + ": " + _("Invalid command") + " `" + cmd + "'");
        }

        Exiv2::TypeId defaultType = Exiv2::invalidTypeId;
        std::string key(line.substr(keyStart, keyEnd - keyStart));
        MetadataId metadataId = invalidMetadataId;
        if (cmdId != reg) {
            try {
                Exiv2::IptcKey iptcKey(key);
                metadataId = iptc;
                defaultType = Exiv2::IptcDataSets::dataSetType(iptcKey.tag(), iptcKey.record());
            } catch (const Exiv2::AnyError&) {
            }
            if (metadataId == invalidMetadataId) {
                try {
                    Exiv2::ExifKey exifKey(key);
                    metadataId = exif;
                    defaultType = exifKey.defaultTypeId();
                } catch (const Exiv2::AnyError&) {
                }
            }
            if (metadataId == invalidMetadataId) {
                try {
                    Exiv2::XmpKey xmpKey(key);
                    metadataId = xmp;
                    defaultType = Exiv2::XmpProperties::propertyType(xmpKey);
                } catch (const Exiv2::AnyError&) {
                }
            }
            if (metadataId == invalidMetadataId) {
                throw Exiv2::Error(Exiv2::kerErrorMessage,
                                   Exiv2::toString(num) + ": " + _("Invalid key") + " `" + key + "'");
            }
        }
        std::string value;
        Exiv2::TypeId type = defaultType;
        bool explicitType = false;
        if (cmdId != del) {
            // Get type and value
            std::string::size_type typeStart = std::string::npos;
            if (keyEnd != std::string::npos)
                typeStart = line.find_first_not_of(delim, keyEnd + 1);
            std::string::size_type typeEnd = std::string::npos;
            if (typeStart != std::string::npos)
                typeEnd = line.find_first_of(delim, typeStart + 1);
            std::string::size_type valStart = typeStart;
            std::string::size_type valEnd = std::string::npos;
            if (valStart != std::string::npos)
                valEnd = line.find_last_not_of(delim);

            if (cmdId == reg && (keyEnd == std::string::npos || valStart == std::string::npos)) {
                throw Exiv2::Error(Exiv2::kerErrorMessage,
                                   Exiv2::toString(num) + ": " + _("Invalid command line") + " ");
            }

            if (cmdId != reg && typeStart != std::string::npos && typeEnd != std::string::npos) {
                std::string typeStr(line.substr(typeStart, typeEnd - typeStart));
                Exiv2::TypeId tmpType = Exiv2::TypeInfo::typeId(typeStr);
                if (tmpType != Exiv2::invalidTypeId) {
                    valStart = line.find_first_not_of(delim, typeEnd + 1);
                    if (valStart == std::string::npos) {
                        throw Exiv2::Error(Exiv2::kerErrorMessage,
                                           Exiv2::toString(num) + ": " + _("Invalid command line") + " ");
                    }
                    type = tmpType;
                    explicitType = true;
                }
            }

            if (valStart != std::string::npos) {
                value = parseEscapes(line.substr(valStart, valEnd + 1 - valStart));
                std::string::size_type last = value.length() - 1;
                if ((value[0] == '"' && value[last] == '"') || (value[0] == '\'' && value[last] == '\'')) {
                    value = value.substr(1, value.length() - 2);
                }
            }
        }

        modifyCmd.cmdId_ = cmdId;
        modifyCmd.key_ = key;
        modifyCmd.metadataId_ = metadataId;
        modifyCmd.typeId_ = type;
        modifyCmd.explicitType_ = explicitType;
        modifyCmd.value_ = value;

        if (cmdId == reg) {
            // Registration needs to be done immediately as the new namespaces are
            // looked up during parsing of subsequent lines (to validate XMP keys).
            Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_);
        }

        return true;
    }  // parseLine

    bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles)
    {
        for (const auto& filename: cmdFiles) {
            try {
                std::ifstream file(filename.c_str());
                bool bStdin = filename.compare("-") == 0;
                if (!file && !bStdin) {
                    std::cerr << filename << ": " << _("Failed to open command file for reading\n");
                    return false;
                }
                int num = 0;
                std::string line;
                while (bStdin ? std::getline(std::cin, line) : std::getline(file, line)) {
                    ModifyCmd modifyCmd;
                    if (parseLine(modifyCmd, line, ++num)) {
                        modifyCmds.push_back(modifyCmd);
                    }
                }
            } catch (const Exiv2::AnyError& error) {
                std::cerr << filename << ", " << _("line") << " " << error << "\n";
                return false;
            }
        }
        return true;
    }  // parseCmdFile

    bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines)
    {
        try {
            int num = 0;
            for (auto line = cmdLines.cbegin(); line != cmdLines.cend(); ++line) {
                ModifyCmd modifyCmd;
                if (parseLine(modifyCmd, *line, ++num)) {
                    modifyCmds.push_back(modifyCmd);
                }
            }
            return true;
        } catch (const Exiv2::AnyError& error) {
            std::cerr << _("-M option") << " " << error << "\n";
            return false;
        }
    }  // parseCmdLines

    int parseCommonTargets(const std::string& optarg, const std::string& action)
    {
        int rc = 0;
        int target = 0;
        int all = Params::ctExif | Params::ctIptc | Params::ctComment | Params::ctXmp;
        int extra = Params::ctXmpSidecar | Params::ctExif | Params::ctIptc | Params::ctXmp;
        for (size_t i = 0; rc == 0 && i < optarg.size(); ++i) {
            switch (optarg[i]) {
                case 'e':
                    target |= Params::ctExif;
                    break;
                case 'i':
                    target |= Params::ctIptc;
                    break;
                case 'x':
                    target |= Params::ctXmp;
                    break;
                case 'c':
                    target |= Params::ctComment;
                    break;
                case 't':
                    target |= Params::ctThumb;
                    break;
                case 'C':
                    target |= Params::ctIccProfile;
                    break;
                case 'I':
                    target |= Params::ctIptcRaw;
                    break;
                case '-':
                    target |= Params::ctStdInOut;
                    break;
                case 'a':
                    target |= all;
                    break;
                case 'X':
                    target |= extra;  // -eX
                    if (i > 0) {      // -eXX or -iXX
                        target |= Params::ctXmpRaw;
                        target &= ~extra;  // turn off those bits
                    }
                    break;

                case 'p': {
                    if (strcmp(action.c_str(), "extract") == 0) {
                        i += (size_t)parsePreviewNumbers(Params::instance().previewNumbers_, optarg, (int)i + 1);
                        target |= Params::ctPreview;
                        break;
                    }
                    printUnrecognizedArgument(optarg[i], action);
                    rc = -1;
                    break;
                }
                default:
                    printUnrecognizedArgument(optarg[i], action);
                    rc = -1;
                    break;
            }
        }
        return rc ? rc : target;
    }

    int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optarg, int j)
    {
        size_t k = j;
        for (size_t i = j; i < optarg.size(); ++i) {
            std::ostringstream os;
            for (k = i; k < optarg.size() && isdigit(optarg[k]); ++k) {
                os << optarg[k];
            }
            if (k > i) {
                bool ok = false;
                int num = Exiv2::stringTo<int>(os.str(), ok);
                if (ok && num >= 0) {
                    previewNumbers.insert(num);
                } else {
                    std::cerr << Params::instance().progname() << ": " << _("Invalid preview number") << ": " << num
                              << "\n";
                }
                i = k;
            }
            if (!(k < optarg.size() && optarg[i] == ','))
                break;
        }
        int ret = static_cast<int>(k - j);
        if (ret == 0) {
            previewNumbers.insert(0);
        }
#ifdef EXIV2_DEBUG_MESSAGES
        std::cout << "\nThe set now contains: ";
        for (auto i = previewNumbers.cbegin(); i != previewNumbers.cend(); ++i) {
            std::cout << *i << ", ";
        }
        std::cout << std::endl;
#endif
        return (int)(k - j);
    }  // parsePreviewNumbers

    std::string parseEscapes(const std::string& input)
    {
        std::string result = "";
        for (unsigned int i = 0; i < input.length(); ++i) {
            char ch = input[i];
            if (ch != '\\') {
                result.push_back(ch);
                continue;
            }
            int escapeStart = i;
            if (!(input.length() - 1 > i)) {
                result.push_back(ch);
                continue;
            }
            ++i;
            ch = input[i];
            switch (ch) {
                case '\\':  // Escaping of backslash
                    result.push_back('\\');
                    break;
                case 'r':  // Escaping of carriage return
                    result.push_back('\r');
                    break;
                case 'n':  // Escaping of newline
                    result.push_back('\n');
                    break;
                case 't':  // Escaping of tab
                    result.push_back('\t');
                    break;
                case 'u':  // Escaping of unicode
                    if (input.length() - 4 > i) {
                        int acc = 0;
                        for (int j = 0; j < 4; ++j) {
                            ++i;
                            acc <<= 4;
                            if (input[i] >= '0' && input[i] <= '9') {
                                acc |= input[i] - '0';
                            } else if (input[i] >= 'a' && input[i] <= 'f') {
                                acc |= input[i] - 'a' + 10;
                            } else if (input[i] >= 'A' && input[i] <= 'F') {
                                acc |= input[i] - 'A' + 10;
                            } else {
                                acc = -1;
                                break;
                            }
                        }
                        if (acc == -1) {
                            result.push_back('\\');
                            i = escapeStart;
                            break;
                        }

                        std::string ucs2toUtf8 = "";
                        ucs2toUtf8.push_back((char)((acc & 0xff00) >> 8));
                        ucs2toUtf8.push_back((char)(acc & 0x00ff));

                        if (Exiv2::convertStringCharset(ucs2toUtf8, "UCS-2BE", "UTF-8")) {
                            result.append(ucs2toUtf8);
                        }
                    } else {
                        result.push_back('\\');
                        result.push_back(ch);
                    }
                    break;
                default:
                    result.push_back('\\');
                    result.push_back(ch);
            }
        }
        return result;
    }

    int readFileToBuf(FILE* f, Exiv2::DataBuf& buf)
    {
        const int buff_size = 4 * 1028;
        Exiv2::byte* bytes = (Exiv2::byte*)::malloc(buff_size);
        int nBytes = 0;
        bool more = bytes != nullptr;
        while (more) {
            char buff[buff_size];
            int n = (int)fread(buff, 1, buff_size, f);
            more = n > 0;
            if (more) {
                bytes = (Exiv2::byte*)realloc(bytes, nBytes + n);
                memcpy(bytes + nBytes, buff, n);
                nBytes += n;
            }
        }

        if (nBytes) {
            buf.alloc(nBytes);
            memcpy(buf.pData_, (const void*)bytes, nBytes);
        }
        if (bytes != nullptr)
            ::free(bytes);
        return nBytes;
    }

    CmdId commandId(const std::string& cmdString)
    {
        int i = 0;
        for (; cmdIdAndString[i].cmdId_ != invalidCmdId && cmdIdAndString[i].cmdString_ != cmdString; ++i) {
        }
        return cmdIdAndString[i].cmdId_;
    }
}  // namespace
